﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using JasLib.Extensions;
using JasLib.Interop.Win32;

namespace JasLib.Interop
{
    /***************************************************************************/
    /// <summary>
    /// This is not the most efficient but it is certainly the most convenient,
    /// and allows arbitrary bit conversion in other generic methods.
    /// Follows the pattern of Enumerable.Empty&lt;T&gt;() and Comparer.Default&lt;T&gt;().
    /// </summary>
    public static class UniversalConverter
    {
        /***************************************************************************/
        private static readonly MethodInfo s_enumerableGetBytesMethodInfo = null;
        private static readonly MethodInfo s_scalarGetBytesMethodInfo =
            typeof(UniversalConverter)
            .GetAllMethods()
            .FirstOrDefault(m => m.Name == "GetBytes" && m.GetGenericArguments().Length == 1);
        private static readonly ConcurrentDictionary<Type, MethodInfo> s_generatedMethodInfoDictionary_GetBytes = new ConcurrentDictionary<Type, MethodInfo>();

        private static readonly MethodInfo s_enumerableFromBytesMethodInfo = null;
        private static readonly MethodInfo s_scalarFromBytesMethodInfo =
            typeof(UniversalConverter)
            .GetAllMethods()
            .FirstOrDefault(m => m.Name == "FromBytes" && m.GetGenericArguments().Length == 1);
        private static readonly ConcurrentDictionary<Type, MethodInfo> s_fromBytesMethodInfoDictionary = new ConcurrentDictionary<Type, MethodInfo>();

        /***************************************************************************/
        /// <summary>
        /// This prevents redundant empty arrays from lingering around.
        /// </summary>
        private static readonly byte[] s_emptyByteArray = new byte[0];

        public static byte[] EmptyByteArray
        {
            get { return s_emptyByteArray; }
        }

        /***************************************************************************/
        private static class ConverterToString<T>
        {
            private static Func<T, string, string> s_func = null;
            public static Func<T, string, string> Convert
            {
                get
                {
                    if (s_func == null)
                        s_func = (v, str) => v.ToString();

                    return s_func;
                }
                set
                {
                    s_func = value;
                }
            }
        }

        /***************************************************************************/
        private static class ConverterFromString<T>
        {
            public delegate bool TryParse(string strFormat, out T value);
            private static TryParse s_func = null;
            public static TryParse Convert
            {
                get
                {
                    /*if (s_convertFunc == null)
                    {
                        /// If null, then it means we don't have custom support;
                        /// handle the general case via marshaling.
                        s_convertFunc =
                            delegate(T value)
                            {
                                byte[] buffer = new byte[MarshalHelper.SizeOf<T>()];
                                unsafe
                                {
                                    fixed (byte* pBuffer = buffer)
                                        Marshal.StructureToPtr(value, (IntPtr)pBuffer, true);
                                }
                                return buffer;
                            };
                    }*/

                    return s_func;
                }
                set
                {
                    s_func = value;
                }
            }
        }

        /***************************************************************************/
        private static class ConverterToBytes<T>
        {
            private static Func<T, byte[]> s_convertFunc = null;
            public static Func<T, byte[]> Convert
            {
                get
                {
                    if (s_convertFunc == null)
                    {
                        /// If null, then it means we don't have custom support;
                        /// handle the general case via marshaling.
                        s_convertFunc =
                            delegate(T value)
                            {
                                byte[] buffer = new byte[MarshalHelper.SizeOf<T>()];
                                unsafe
                                {
                                    fixed (byte* pBuffer = buffer)
                                        Marshal.StructureToPtr(value, (IntPtr)pBuffer, true);
                                }
                                return buffer;
                            };
                    }

                    return s_convertFunc;
                }
                set
                {
                    s_convertFunc = value;
                }
            }
        }

        /***************************************************************************/
        private static class ConverterFromBytes<T>
        {
            private static Func<byte[], int, T> s_convertFunc = null;
            public static Func<byte[], int, T> Convert
            {
                get
                {
                    if (s_convertFunc == null)
                    {
                        /// If null, then it means we don't have custom support;
                        /// handle the general case via marshaling.
                        s_convertFunc =
                            delegate(byte[] buffer, int iStartIndex)
                            {
                                unsafe
                                {
                                    fixed (byte* pBuffer = buffer)
                                        return (T)Marshal.PtrToStructure((UINT_PTR)pBuffer + iStartIndex, typeof(T));
                                }
                            };
                    }

                    return s_convertFunc;
                }
                set
                {
                    s_convertFunc = value;
                }
            }
        }

        /***************************************************************************/
        static UniversalConverter()
        {
            /// We can't typecast T to a specific type without boxing,
            /// if we do these assignments inside the generic classes.

            ConverterToString<sbyte>.Convert = (v, str) => v.ToString(str);
            ConverterToString<byte>.Convert = (v, str) => v.ToString(str);
            ConverterToString<short>.Convert = (v, str) => v.ToString(str);
            ConverterToString<ushort>.Convert = (v, str) => v.ToString(str);
            ConverterToString<int>.Convert = (v, str) => v.ToString(str);
            ConverterToString<uint>.Convert = (v, str) => v.ToString(str);
            ConverterToString<long>.Convert = (v, str) => v.ToString(str);
            ConverterToString<ulong>.Convert = (v, str) => v.ToString(str);
            ConverterToString<float>.Convert = (v, str) => v.ToString(str);
            ConverterToString<double>.Convert = (v, str) => v.ToString(str);
            ConverterToString<string>.Convert = (v, str) => v;
            ConverterToString<DateTime>.Convert = (v, str) => v.ToString(str);
            ConverterToString<TimeSpan>.Convert = (v, str) => v.ToString(str);

            ConverterFromString<sbyte>.Convert = sbyte.TryParse;
            ConverterFromString<byte>.Convert = byte.TryParse;
            ConverterFromString<bool>.Convert = bool.TryParse;
            ConverterFromString<char>.Convert = char.TryParse;
            ConverterFromString<short>.Convert = short.TryParse;
            ConverterFromString<ushort>.Convert = ushort.TryParse;
            ConverterFromString<int>.Convert = int.TryParse;
            ConverterFromString<uint>.Convert = uint.TryParse;
            ConverterFromString<long>.Convert = long.TryParse;
            ConverterFromString<ulong>.Convert = ulong.TryParse;
            ConverterFromString<float>.Convert = float.TryParse;
            ConverterFromString<double>.Convert = double.TryParse;
            ConverterFromString<string>.Convert = (string strIn, out string strOut) => { strOut = strIn; return true; };
            ConverterFromString<DateTime>.Convert = DateTime.TryParse;
            ConverterFromString<TimeSpan>.Convert = TimeSpan.TryParse;

            ConverterToBytes<sbyte>.Convert = (v) => new byte[1] { (byte)v };
            ConverterToBytes<byte>.Convert = (v) => new byte[1] { v };
            ConverterToBytes<bool>.Convert = BitConverter.GetBytes;
            ConverterToBytes<char>.Convert = BitConverter.GetBytes;
            ConverterToBytes<short>.Convert = BitConverter.GetBytes;
            ConverterToBytes<ushort>.Convert = BitConverter.GetBytes;
            ConverterToBytes<int>.Convert = BitConverter.GetBytes;
            ConverterToBytes<uint>.Convert = BitConverter.GetBytes;
            ConverterToBytes<long>.Convert = BitConverter.GetBytes;
            ConverterToBytes<ulong>.Convert = BitConverter.GetBytes;
            ConverterToBytes<float>.Convert = BitConverter.GetBytes;
            ConverterToBytes<double>.Convert = BitConverter.GetBytes;
            ConverterToBytes<string>.Convert = Encoding.Unicode.GetBytes;
            ConverterToBytes<DateTime>.Convert = (v) => BitConverter.GetBytes(v.Ticks);
            ConverterToBytes<TimeSpan>.Convert = (v) => BitConverter.GetBytes(v.Ticks);
            ConverterToBytes<byte[]>.Convert = (v) =>
            {
                byte[] copy = new byte[v.Length];
                Buffer.BlockCopy(v, 0, copy, 0, copy.Length);
                return copy;
            };

            ConverterFromBytes<sbyte>.Convert = (b, i) => (sbyte)b[i];
            ConverterFromBytes<byte>.Convert = (b, i) => b[i];
            ConverterFromBytes<bool>.Convert = BitConverter.ToBoolean;
            ConverterFromBytes<char>.Convert = BitConverter.ToChar;
            ConverterFromBytes<short>.Convert = BitConverter.ToInt16;
            ConverterFromBytes<ushort>.Convert = BitConverter.ToUInt16;
            ConverterFromBytes<int>.Convert = BitConverter.ToInt32;
            ConverterFromBytes<uint>.Convert = BitConverter.ToUInt32;
            ConverterFromBytes<long>.Convert = BitConverter.ToInt64;
            ConverterFromBytes<ulong>.Convert = BitConverter.ToUInt64;
            ConverterFromBytes<float>.Convert = BitConverter.ToSingle;
            ConverterFromBytes<double>.Convert = BitConverter.ToDouble;
            ConverterFromBytes<string>.Convert = (b, i) => Encoding.Unicode.GetString(b, i, b.Length - i);
            ConverterFromBytes<DateTime>.Convert = (b, i) => new DateTime(BitConverter.ToInt64(b, i));
            ConverterFromBytes<TimeSpan>.Convert = (b, i) => new TimeSpan(BitConverter.ToInt64(b, i));
            ConverterFromBytes<byte[]>.Convert = (b, i) =>
            {
                byte[] copy = new byte[b.Length - i];
                Buffer.BlockCopy(b, i, copy, 0, copy.Length);
                return copy;
            };

            return;
        }

        /***************************************************************************/
        /// <remarks>
        /// This method is used in reflection. Do not rename or overload this without understanding the consequences.
        /// </remarks>
        public static string ToString<T>(T value, string strFormat)
        {
            return ConverterToString<T>.Convert(value, strFormat);
        }

        /***************************************************************************/
        /// <remarks>
        /// This method is used in reflection. Do not rename or overload this without understanding the consequences.
        /// </remarks>
        public static bool TryParse<T>(string strValue, out T value)
        {
            return ConverterFromString<T>.Convert(strValue, out value);
        }

        /***************************************************************************/
        /// <summary>
        /// Converts the variable into a native stream of bytes.
        /// </summary>
        /// <remarks>
        /// This method is used in reflection. Do not rename or overload this without understanding the consequences.
        /// </remarks>
        public static byte[] GetBytes<T>(this T value)
        {
            byte[] buffer = ConverterToBytes<T>.Convert(value);

            if (buffer == null || buffer.Length == 0)
                return s_emptyByteArray;
            else
                return buffer;
        }

        /***************************************************************************/
        /// <summary>
        /// Treats the IEnumerable&lt;T&gt; as an array and converts it to a contiguous native stream of bytes.
        /// </summary>
        public static byte[] GetArrayBytes<T>(this IEnumerable<T> values)
        {
            MemoryStream stream = new MemoryStream();
            foreach (T value in values)
            {
                byte[] buffer = GetBytes(value);
                stream.Write(buffer, 0, buffer.Length);
            }

            if (stream.Length == 0)
                return s_emptyByteArray;
            else
                return stream.ToArray();
        }

        /***************************************************************************/
        /// <summary>
        /// Use this for when you don't know the type at compile time.
        /// </summary>
        public static byte[] GetBytesFromBoxedType(object objValue)
        {
            /// This also handles nullable values set to null.
            /// Remember that when converting Nullable<> to object,
            /// the Nullable<> is stripped out by the CLR and just converted to
            /// a null reference or the boxed struct that was inside.
            if (objValue == null)
                return null;

            Type objectType = objValue.GetType();
            if (typeof(IEnumerable<>).IsAssignableFrom(objectType))
            {
                /// TODO: Not finished yet.
                throw new NotImplementedException();
            }
            else
            {
                if (objectType.IsType(typeof(System.Enum)))
                {
                    objectType = Enum.GetUnderlyingType(objectType);
                    /// TODO: Cast the value to the underlying type too?
                }

                MethodInfo getBytesMethodInfo = s_generatedMethodInfoDictionary_GetBytes.GetOrAdd(
                    objectType,
                    t => s_scalarGetBytesMethodInfo.MakeGenericMethod(objectType));
                return (byte[])getBytesMethodInfo.Invoke(null, new object[] { objValue });
            }
        }

        /***************************************************************************/
        public static object GetBoxedTypeFromBytes(Type objectType, byte[] buffer, UINT_PTR uiOffset = default(UINT_PTR))
        {
            if (buffer == null || objectType == null)
                return null;

            if (typeof(IEnumerable<>).IsAssignableFrom(objectType))
            {
                /// TODO: Not finished yet.
                throw new NotImplementedException();
            }
            else
            {
                MethodInfo fromBytesMethodInfo = s_fromBytesMethodInfoDictionary.GetOrAdd(
                    objectType,
                    t => s_scalarFromBytesMethodInfo.MakeGenericMethod(objectType));
                return fromBytesMethodInfo.Invoke(null, new object[] { buffer, uiOffset });
            }
        }

        /***************************************************************************/
        /// <summary>
        /// Converts the byte array into any datatype.
        /// </summary>
        public static T FromBytes<T>(this byte[] buffer, UINT_PTR uiStartIndex = default(UINT_PTR))
        {
            return ConverterFromBytes<T>.Convert(buffer, (int)uiStartIndex);
        }

        /***************************************************************************/
        /// <summary>
        /// Converts the byte array into any datatype.
        /// Strings have variable length, hence the restriction to structs.
        /// </summary>
        /// <param name="iCount">The remaining size of the buffer. If -1, it will read to the end.
        /// If there are not {0 < n <= sizeof(T)} bytes remaining for the final element, an array bounds exception will be thrown.</param>
        public static T[] ArrayFromBytes<T>(this byte[] buffer, int iStartIndex = 0, int iCountOfT = -1) where T : struct
        {
            int iSizeOfT = MarshalHelper.SizeOf<T>();
            int iEndPosition = iStartIndex + ((iCountOfT == -1) ? (buffer.Length - iStartIndex) : iCountOfT);

            List<T> values = new List<T>();

            int iPosition = iStartIndex;
            int iElementsRead = 0;
            while (iPosition < iEndPosition && (iCountOfT == -1 || iElementsRead > iCountOfT))
            {
                T value = FromBytes<T>(buffer, iPosition);
                values.Add(value);

                iPosition += iSizeOfT;
                iElementsRead++;
            }

            return values.ToArray();
        }

        /***************************************************************************/
        public static void Read<T>(this MemoryStream stream, out T value)
            where T : struct
        {
            byte[] buffer = new byte[MarshalHelper.SizeOf<T>()];
            stream.Read(buffer, 0, buffer.Length);
            value = FromBytes<T>(buffer, 0);
            return;
        }
    }
}
